/**    
* @Title: ConfSpringbootContextLoader.java  
* @Package com.kaver.conf.springboot.test   
* @author roc   
* @date 2019年8月17日 上午11:05:17  
* @version V1.0  
* @copyright kaver 
*/
package com.kaver.conf.springboot.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.BeanUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.boot.test.context.SpringBootContextLoader;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.web.SpringBootMockServletContext;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.boot.web.support.ServletContextApplicationContextInitializer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.SpringVersion;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySources;
import org.springframework.core.env.PropertySourcesPropertyResolver;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.support.AnnotationConfigContextLoaderUtils;
import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.test.context.web.WebMergedContextConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.context.support.StandardServletEnvironment;

/**  
* @ClassName: ConfSpringbootContextLoader  
* @Description: 自定义  SpringbootContextLoader
* @author roc  
* @date 2019年8月17日 上午11:05:17  
*    
*/
public class ConfSpringbootContextLoader extends SpringBootContextLoader{
    private static final Set<String> INTEGRATION_TEST_ANNOTATIONS;

    static {
        Set<String> annotations = new LinkedHashSet<String>();
        annotations.add("org.springframework.boot.test.IntegrationTest");
        annotations.add("org.springframework.boot.test.WebIntegrationTest");
        INTEGRATION_TEST_ANNOTATIONS = Collections.unmodifiableSet(annotations);
    }

    @Override
    public ApplicationContext loadContext(MergedContextConfiguration config)
            throws Exception {
        SpringApplication application = getSpringApplication();
        application.setMainApplicationClass(config.getTestClass());
        application.setSources(getSources(config));
        ConfigurableEnvironment environment = new StandardServletEnvironment();
        if (!ObjectUtils.isEmpty(config.getActiveProfiles())) {
            setActiveProfiles(environment, config.getActiveProfiles());
        }
        ResourceLoader resourceLoader = (application.getResourceLoader() != null)
                ? application.getResourceLoader()
                : new DefaultResourceLoader(getClass().getClassLoader());
        TestPropertySourceUtils.addPropertiesFilesToEnvironment(environment,
                resourceLoader, config.getPropertySourceLocations());
        TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment,
                getInlinedProperties(config));
        application.setEnvironment(environment);
        List<ApplicationContextInitializer<?>> initializers = getInitializers(config,
                application);
        if (config instanceof WebMergedContextConfiguration) {
            application.setWebEnvironment(true);
            if (!isEmbeddedWebEnvironment(config)) {
                new WebConfigurer().configure(config, application, initializers);
            }
        }
        else {
            application.setWebEnvironment(false);
        }
        application.setInitializers(initializers);
        ConfigurableApplicationContext context = application.run();
        return context;
    }

    /**
     * Builds new {@link org.springframework.boot.SpringApplication} instance. You can
     * override this method to add custom behavior
     * @return {@link org.springframework.boot.SpringApplication} instance
     */
    protected SpringApplication getSpringApplication() {
        return new SpringApplication();
    }

    private Set<Object> getSources(MergedContextConfiguration mergedConfig) {
        Set<Object> sources = new LinkedHashSet<Object>();
        sources.addAll(Arrays.asList(mergedConfig.getClasses()));
        sources.addAll(Arrays.asList(mergedConfig.getLocations()));
        Assert.state(!sources.isEmpty(), "No configuration classes "
                + "or locations found in @SpringApplicationConfiguration. "
                + "For default configuration detection to work you need "
                + "Spring 4.0.3 or better (found " + SpringVersion.getVersion() + ").");
        return sources;
    }

    private void setActiveProfiles(ConfigurableEnvironment environment,
            String[] profiles) {
        EnvironmentTestUtils.addEnvironment(environment, "spring.profiles.active="
                + StringUtils.arrayToCommaDelimitedString(profiles));
    }

    protected String[] getInlinedProperties(MergedContextConfiguration config) {
        ArrayList<String> properties = new ArrayList<String>();
        // JMX bean names will clash if the same bean is used in multiple contexts
        disableJmx(properties);
        properties.addAll(Arrays.asList(config.getPropertySourceProperties()));
        if (!isEmbeddedWebEnvironment(config) && !hasCustomServerPort(properties)) {
            properties.add("server.port=-1");
        }
        return properties.toArray(new String[properties.size()]);
    }

    private void disableJmx(List<String> properties) {
        properties.add("spring.jmx.enabled=false");
    }

    private boolean hasCustomServerPort(List<String> properties) {
        PropertySources sources = convertToPropertySources(properties);
        RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(
                new PropertySourcesPropertyResolver(sources), "server.");
        return resolver.containsProperty("port");
    }

    private PropertySources convertToPropertySources(List<String> properties) {
        Map<String, Object> source = TestPropertySourceUtils
                .convertInlinedPropertiesToMap(
                        properties.toArray(new String[properties.size()]));
        MutablePropertySources sources = new MutablePropertySources();
        sources.addFirst(new MapPropertySource("inline", source));
        return sources;
    }

    private List<ApplicationContextInitializer<?>> getInitializers(
            MergedContextConfiguration config, SpringApplication application) {
        List<ApplicationContextInitializer<?>> initializers = new ArrayList<ApplicationContextInitializer<?>>();
        for (ContextCustomizer contextCustomizer : config.getContextCustomizers()) {
            initializers.add(new ContextCustomizerAdapter(contextCustomizer, config));
        }
        initializers.addAll(application.getInitializers());
        for (Class<? extends ApplicationContextInitializer<?>> initializerClass : config
                .getContextInitializerClasses()) {
            initializers.add(BeanUtils.instantiate(initializerClass));
        }
        if (config.getParent() != null) {
            initializers.add(new ParentContextApplicationContextInitializer(
                    config.getParentApplicationContext()));
        }
        return initializers;
    }

    private boolean isEmbeddedWebEnvironment(MergedContextConfiguration config) {
        for (String annotation : INTEGRATION_TEST_ANNOTATIONS) {
            if (AnnotatedElementUtils.isAnnotated(config.getTestClass(), annotation)) {
                return true;
            }
        }
        SpringBootTest annotation = AnnotatedElementUtils
                .findMergedAnnotation(config.getTestClass(), SpringBootTest.class);
        if (annotation != null && annotation.webEnvironment().isEmbedded()) {
            return true;
        }
        return false;
    }

    @Override
    public void processContextConfiguration(
            ContextConfigurationAttributes configAttributes) {
        super.processContextConfiguration(configAttributes);
        if (!configAttributes.hasResources()) {
            Class<?>[] defaultConfigClasses = detectDefaultConfigurationClasses(
                    configAttributes.getDeclaringClass());
            configAttributes.setClasses(defaultConfigClasses);
        }
    }

    /**
     * Detect the default configuration classes for the supplied test class. By default
     * simply delegates to
     * {@link AnnotationConfigContextLoaderUtils#detectDefaultConfigurationClasses} .
     * @param declaringClass the test class that declared {@code @ContextConfiguration}
     * @return an array of default configuration classes, potentially empty but never
     * {@code null}
     * @see AnnotationConfigContextLoaderUtils
     */
    protected Class<?>[] detectDefaultConfigurationClasses(Class<?> declaringClass) {
        return AnnotationConfigContextLoaderUtils
                .detectDefaultConfigurationClasses(declaringClass);
    }

    @Override
    public ApplicationContext loadContext(String... locations) throws Exception {
        throw new UnsupportedOperationException("SpringApplicationContextLoader "
                + "does not support the loadContext(String...) method");
    }

    @Override
    protected String[] getResourceSuffixes() {
        return new String[] { "-context.xml", "Context.groovy" };
    }

    @Override
    protected String getResourceSuffix() {
        throw new IllegalStateException();
    }

    /**
     * Inner class to configure {@link WebMergedContextConfiguration}.
     */
    private static class WebConfigurer {

        private static final Class<GenericWebApplicationContext> WEB_CONTEXT_CLASS = GenericWebApplicationContext.class;

        void configure(MergedContextConfiguration configuration,
                SpringApplication application,
                List<ApplicationContextInitializer<?>> initializers) {
            WebMergedContextConfiguration webConfiguration = (WebMergedContextConfiguration) configuration;
            addMockServletContext(initializers, webConfiguration);
            application.setApplicationContextClass(WEB_CONTEXT_CLASS);
        }

        private void addMockServletContext(
                List<ApplicationContextInitializer<?>> initializers,
                WebMergedContextConfiguration webConfiguration) {
            SpringBootMockServletContext servletContext = new SpringBootMockServletContext(
                    webConfiguration.getResourceBasePath());
            initializers.add(0, new ServletContextApplicationContextInitializer(
                    servletContext, true));
        }

    }

    /**
     * Adapts a {@link ContextCustomizer} to a {@link ApplicationContextInitializer} so
     * that it can be triggered via {@link SpringApplication}.
     */
    private static class ContextCustomizerAdapter
            implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        private final ContextCustomizer contextCustomizer;

        private final MergedContextConfiguration config;

        ContextCustomizerAdapter(ContextCustomizer contextCustomizer,
                MergedContextConfiguration config) {
            this.contextCustomizer = contextCustomizer;
            this.config = config;
        }

        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {
            this.contextCustomizer.customizeContext(applicationContext, this.config);
        }

    }

    @Order(Ordered.HIGHEST_PRECEDENCE)
    private static class ParentContextApplicationContextInitializer
            implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        private final ApplicationContext parent;

        ParentContextApplicationContextInitializer(ApplicationContext parent) {
            this.parent = parent;
        }

        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {
            applicationContext.setParent(this.parent);
        }

    }
}
